Tomcat の修正 Commit を読んで理解する CVE-2021-43980

Tomcat の修正 Commit を読んで理解する CVE-2021-43980

Clock Icon2022.10.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは。小室@さっぽろです。2022/09/30 Tomcat で新しい脆弱性が JVN のサイトにて公開されました。

JVNVU#98868043 - Apache TomcatにHttp11Processorインスタンスにおける競合状態による情報漏えいの脆弱性

複数のクライアントから接続された場合、Http11Processorのインスタンスを共有していることに起因して、レスポンスのすべてまたはその一部を誤ったクライアントに送信するため、情報が漏えいする可能性があります。

という内容でなかなかな内容です。

気になる脆弱性であったため 趣味で コードを見てみました。Tomcat のコードを追うのは初めてなので理解が間違っているかもしれません。 理解が間違っているところや曖昧な箇所があれば指摘ください。 *1

「なぜそうなるのか」まではある程度調べたものの、再現方法に関しては調べられていません。そのため内容としてはとても中途半端です。読む際には予めご了承ください。

脆弱性公開までのタイムラインが気になる

2022/09/30 JVN で公開されたため、最近発見された脆弱性なのかと思ったのですが、タイムラインを確認してみたところそうでもなさそうでした。 Twitter での指摘を見て確認してみたのですが、確かに公開までそこそこの年月を要していたようです。

CVEの割当が2021/11/17https://t.co/w1pawY8KI5
Tomcatが対応したのが2022/04/01https://t.co/RH0NzZBcB7
詳細がpublicになったのが2022/09/28https://t.co/z7yeYapMyT
JVNが公開されたのが2022/09/30https://t.co/LblOAVTxIF
みたいな時系列?(よくわかってない)

— kazuhiro1982 (@kappe1982) September 30, 2022

  1. CVE 採番されたのが 2021年の11月 - CVE-2021-43980
  2. 修正は 2022年の4月 - Fixed in Apache Tomcat 10.0.20
  3. NVD で公開されたのが2022/09/28 - CVE-2021-43980 Detail
  4. JVN で公開されたのが2022/09/30 - Apache TomcatにHttp11Processorインスタンスにおける競合状態による情報漏えいの脆弱性

CVE が採番されてからおよそ 1 年越しの公開のようです。

修正バージョン

Tomcat はすでに修正バージョンがリリースされてしばらく経過しているため、最新バージョンにあげていれば Tomcat 8, 9, 10 いずれでも問題がないようです。

  • Apache Tomcat 10.1.0-M14およびそれ以降のバージョン
  • Apache Tomcat 10.0.20およびそれ以降の以降のバージョン
  • Apache Tomcat 9.0.62およびそれ以降の以降のバージョン
  • Apache Tomcat 8.5.78およびそれ以降の以降のバージョン

ただし、影響バージョンを確認するに例えば 9.x 系であれば 9.0.62 以前のものはすべて影響を受けるようなので、注意は必要です。

Affects: 9.0.0-M1 to 9.0.60

修正内容

Tomcat は以下の Commit ですでに修正されています。 当該 Commit の日付は 30 Mar となっており、4月頃にはすべて対応が完了していたことの裏付けになりそうです。

Commit#9651b83a1d04583791525e5f0c4c9089f678d9fc

修正内容について確認します。以下のファイルが変更されていました。

  • java/org/apache/coyote/AbstractProtocol.java
  • java/org/apache/tomcat/util/net/SocketWrapperBase.java

SocketWrapperBase が保持していた Processor オブジェクトを AbstractProtocol 内で利用して処理を行っているようです。

しかし元の実装を見ていると、processsor を取得して処理を開始し、あとで null を再セットするという処理になっています。確かにこの実装だと process を取得して処理し始めてから null をセットするまでの間、wrapper.getCurrentProcessor() を呼び出すところで同じオブジェクトの参照を取得することができてしまいそうです。

Processor processor = (Processor) wrapper.getCurrentProcessor();

してから

wrapper.setCurrentProcessor(null);
release(processor);

この辺が呼ばれるまでの間、 .getCurrentProcessor() は同じものを返しそう。 そこで修正は SocketWrapperBaseprocessor の保持方法と .getCurrentProcessor() の改修及び、.takeCurrentProcessor() というメソッドを新設しています。

private final AtomicReference<Object> currentProcessor = new AtomicReference<>();

public Object getCurrentProcessor() {
    return currentProcessor.get();
}

public void setCurrentProcessor(Object currentProcessor) {
    this.currentProcessor.set(currentProcessor);
}

public Object takeCurrentProcessor() {
    return currentProcessor.getAndSet(null);
}

AtomicReference を利用してオブジェクトの原子性を保って更新や取得ができるように変更。 さらに .takeCurrentProcessor() を新たに作成し、 Processor の単一のオブジェクトを取得しつつ、 null を Atomic にセットすることでおかしな共有ができないよう対処したようです。 .getCurrentProcessor()AtomicReference.get() を利用するよう変更されています。

さらに AbstractProtocol の中で .getCurrentProcessor() を呼んでいた箇所も .takeCurrentProcessor() に変更し、 Processor オブジェクトが共有状態にならないよう改修したようです。

なるほど。これだと複数回呼ばれたとしても、1回目は取得でき、2回目は null が返るため、意図しない共有は発生しない、というのは理解できます。

Java SE 11 & JDK 11 - AtomicReference

なぜ Http11Processor だけなのか?

NVD の文章を確認すると

that could cause client connections to share an Http11Processor instance resulting in responses, or part responses, to be received by the wrong client.

とあります。JVN でも同じく

複数のクライアントから接続された場合、Http11Processorのインスタンスを共有していることに起因して、レスポンスのすべてまたはその一部を誤ったクライアントに送信するため、情報が漏えいする可能性があります。

原因は Http11Processor インスタンスを共有していること、とあるにも関わらず、修正は AbstractProtocolSocketWrapperBase です。なぜこれで問題なくなるのかがいまいち釈然としません。

おそらく Http11Processor はなんらかのインタフェースの実実装であり、他にHTTP/2等の別プロトコルの実実装もあるはずです。にも関わらず HTTP/1.1 の実実装だけ名指しされているのか修正内容から判断できませんでした。

そこで修正されたファイルを中心に周囲のコードを読んでみます。 *2

AbstractProtocol と Http11Processor の関連

AbstractProtocol のコードを読んで見たところ、内部で Http11Processor を直接呼び出している箇所はありませんでした。しかし、 Processor インタフェースを型として期待している変数がいくつか存在します(正確には Inner Class)

Github - apache/tomcat - AbstractProtocol.java#L89-L90

private final Set<Processor> waitingProcessors =
        Collections.newSetFromMap(new ConcurrentHashMap<>());

Github - apache/tomcat - AbstractProtocol.java#L1162 Github - apache/tomcat - AbstractProtocol.java#L1162

private final RecycledProcessors recycledProcessors = new RecycledProcessors(this);
...

protected static class RecycledProcessors extends SynchronizedStack<Processor> {
....

直接保持しているものもあれば、 Inner Class として定義してその中に内包しているものもあるようですが、ひとまず AbstractProtocolProcessor の関連は存在する事はわかりました。

大枠の予想

少し予想をたててコードを追っていきます。

  • Http11Processor はおそらく HTTP/1.1 に関わる実処理を行う Processor 実装
  • AbstractProtocol には Http11Processor の継承元になるインタフェースもしくは AbstractClass をインスタンスとして保持している箇所がある
  • AbstractProtocol の処理のなかで HTTP/1.1 の実実装だけ、問題となる処理に関わる何かを行っている箇所がある

Http11Processor

Github - apache/tomcat - Http11Processor.java

public class Http11Processor extends AbstractProcessor {

Github - apache/tomcat - AbstractProcessor.java

public abstract class AbstractProcessor extends AbstractProcessorLight implements ActionHook {

Github - apache/tomcat - AbstractProcessorLight.java

public abstract class AbstractProcessorLight implements Processor {

ありました。

Http11Processor extends AbstractProcessor extends AbstractProcessorLight implements Processor なので Processor インタフェースの変数に Http11Processor インスタンスを投入できそう。

AbstractProtocol を読んでいく

恐らくはここにヒントがあるはず。

Github - apache/tomcat - AbstractProtocol.java#L777

Processor processor = (Processor) wrapper.takeCurrentProcessor();

mainブランチのコードなので、すでに修正済みのコードになります。

processor を取得し、存在しない場合の分岐は L805へ

Github - apache/tomcat - AbstractProtocol.java#L805

if (processor == null) {
    String negotiatedProtocol = wrapper.getNegotiatedProtocol();

この中を見てると http/1.1 の分岐があるのがわかります(L816)

} else if (negotiatedProtocol.equals("http/1.1")) {
    // Explicitly negotiated the default protocol.
    // Obtain a processor below.
}

if 内で何もしていないので processornull のまま。 return もしないので処理継続。

if (processor == null) {
    processor = recycledProcessors.pop();
    if (getLog().isDebugEnabled()) {
        getLog().debug(sm.getString("abstractConnectionHandler.processorPop", processor));
    }
}
if (processor == null) {
    processor = getProtocol().createProcessor();
    register(processor);
    if (getLog().isDebugEnabled()) {
        getLog().debug(sm.getString("abstractConnectionHandler.processorCreate", processor));
    }
}

recycledProcessors という Stack から再利用可能な processor を探し、存在しなければ新規作成という処理を行っていそうです。この時点で processornull であることはなさそう。 その後 null ではない Processor インタフェースを継承した何かしらの実実装オブジェクトの process() を呼び出しているのがわかります。

do {
    state = processor.process(wrapper, status);

元の .getCurrentProcessor() で意図しないインスタンスが返ってきたと考えると、本来は2回目の処理では、recycledProcessors.pop() によるインスタンスの再利用、もしくは .createProcessor() をしなければならないにもかかわらず、ここがスキップされ、1回目の処理で利用している同じ processor に対して処理を行ってしまいます。これにより、 Response が混線するといった事象が発生するのではないかと思われます。

なんとなく流れはわかったが根本原因や再現方法は?

とりあえず、ここまではない頭を捻りつつコードを読んでなんとなく自分なりの理解をしました。

次はこれがどのようなときに発生するのか?を見る必要がありそうなんですが、コード上追うのがかなり厳しくなってきたので(脳の理解が追いつかない) IDE を使ってステップ実行等で実行しながら見ないと理解ができなさそう。

ひとまず外部から任意で攻撃を実行できるようなものではないようですが、どうしても気になるのでなんらか検証方法を思いついたらどこかで調べようかと思います(ギブアップ)

残件

以下今回調べきれなかった件です。

  • 複数の Client が接続された時、 具体的にどういった処理が行われて AbstractProtocol の当該箇所が実行されるのか
  • Http11Processor 以外の Processor 実装の際にどのルートを通り、 processor がどのように変化するか
  • wrapper.getCurrentProcessor() で複数回同じ processor が取得できる再現ケース
  • AbstractProtocol が実際どのように利用され、リクエストを受けている Tomcat でどのように動作しているのか、リクエスト → レスポンスまでの間にどのような処理が行われるのかの詳細

そのうち続きを調べるかもしれません。。

HTTP/2 の Processor 実装は StreamProcessor っぽい. https://github.com/apache/tomcat/blob/main/java/org/apache/coyote/http2/StreamProcessor.java

まとめ

内容がなかなか強烈な脆弱性であったため、説明読んだ際はギョッとしたのですが、どうも外部から任意で実行できるようなものではなさそうということから、即時サービス停止で対応というものにはなりませんでした。

が、一度でも発生してしまうととてもマズイ内容であるため、早めの対応が必要なことは間違いありません。我々のサービスで利用している SpringBoot の組み込み Tomcat は Framework の推移依存で入ってくるものではなく、マイナーバージョン、パッチバージョンを最新に上げたものを提供しているため、すでに対応済みバージョンが適用されていました。 ただ、古い環境の場合等は、アップデートが追随できておらず未だ影響を受けるバージョンが稼働しているところもあるため、急ぎ調査が必要になります。

今回 Tomcat のコードを初めて眺めてみました。歴史が古いのもあると思うのですが、1ファイルあたりのコード量がなかなか多くコードを読み解こうとするとなかなか骨が折れる作業でした。しかし、普段大変お世話になっているアプリケーション動作基盤のコードを読む機会は、そうそうないので今回の件は読み始める良いキッカケになりました。

今後も継続できるかは微妙なところですが、こういった機会(あんまり脆弱性がたくさんあっても困るんですが)を利用して少しずつ読めるようにしていきたいところです。 間違ってる可能性も大いにあるので、なにか間違って誤解を招きそうであれば取り下げるかもしれません。

誰か一緒に Tomcat のコードを読んで全体を解明してくれる人募集してます。

参照リンク

脚注

  1. 素人の感想くらいの温度感で見ていただければ・・・。
  2. 本来は `AbstractProtocol` がそもそもどのように使われ、動作するかも確認するべき

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.